Golang ServeMux 是如何实现多路处理的
1. 简介
net/http 包里的 server.go 文件里注释写着:ServeMux is an HTTP request multiplexer. 即 ServeMux 是一个 HTTP 请求的 “多路处理器”,因为 ServeMux 实现的功能就是将收到的 HTTP 请求的 URL 与注册的路由相匹配,选择匹配度最高的路由的处理函数来处理该请求。
最简单的栗子:
1 2 3 4
| mux := http.NewServeMux() mux.HandleFunc("/a/b", ab) mux.HandleFunc("/a", a) http.ListenAndServe(":8000", mux)
|
每个路由对应了一个处理函数。
先来看看 NewServeMux 函数
1
| func NewServeMux() *ServeMux { return new(ServeMux) }
|
我们知道 new 函数会为传入的类型分配空间并返回指向该空间首地址的指针,于是我们就获取了一个 ServeMux 实例。
2. 源码分析
1. ServeMux 结构体
1 2 3 4 5 6
| type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry hosts bool }
|
其中 m 就是用来存储路由与处理函数映射关系的 map,es 按照路由长度从大到小的存放处理函数 (后面会讲为什么要这样),但 ServeMux 为了方便,map存放的值其实是放有处理函数和路由路径的 muxEntry 结构体:
1 2 3 4
| type muxEntry struct { h Handler pattern string }
|
ServeMux 暴露的方法主要是下面 4 个:
1 2 3 4
| func (mux *ServeMux) Handle(pattern string, handler Handler) func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)
|
2. Handle 方法
Handle 方法通过将传入的路由和处理函数存入 ServeMux 的映射表 m 中来实现 “路由注册(register)”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| func (mux *ServeMux) Handle(pattern string, handler Handler) { mux.mu.Lock() defer mux.mu.Unlock() if pattern == "" { panic("http: invalid pattern") } if handler == nil { panic("http: nil handler") } if _, exist := mux.m[pattern]; exist { panic("http: multiple registrations for " + pattern) } if mux.m == nil { mux.m = make(map[string]muxEntry) } e := muxEntry{h: handler, pattern: pattern} mux.m[pattern] = e if pattern[len(pattern)-1] == '/' { mux.es = appendSorted(mux.es, e) } if pattern[0] != '/' { mux.hosts = true } }
|
3. HandleFunc 方法
HandleFunc 方法接收一个具体的处理函数将其包装成 Handler:
1 2 3 4 5 6 7 8 9
| type HandlerFunc func(ResponseWriter, *Request)
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { if handler == nil { panic("http: nil handler") } mux.Handle(pattern, HandlerFunc(handler)) }
|
其中 HandlerFunc(f) 起到的作用就是在 HandlerFunc 中执行 f
4. Handler 方法
Handler 方法从传入的请求(Request)中拿到 URL 进行匹配,返回对应的处理函数和路由
在看 Handler 的实现前,先看看它调用的 handler 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { mux.mu.RLock() defer mux.mu.RUnlock()
if mux.hosts { h, pattern = mux.match(host + path) } if h == nil { h, pattern = mux.match(path) } if h == nil { h, pattern = NotFoundHandler(), "" } return }
|
好了,现在是 Handler 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) { host := stripHostPort(r.Host) path := cleanPath(r.URL.Path)
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok { return RedirectHandler(u.String(), StatusMovedPermanently), u.Path }
if path != r.URL.Path { _, pattern = mux.handler(host, path) url := *r.URL url.Path = path return RedirectHandler(url.String(), StatusMovedPermanently), pattern } return mux.handler(host, r.URL.Path) }
|
我们有必要看看 match 方法是怎么进行匹配的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| func (mux *ServeMux) match(path string) (h Handler, pattern string) { v, ok := mux.m[path] if ok { return v.h, v.pattern }
for _, e := range mux.es { if strings.HasPrefix(path, e.pattern) { return e.h, e.pattern } } return nil, "" }
|
注意这里是在 mux.es 中进行查找,而不是映射表 mux.m 中,而 mux.es 是存放所有以 “/“ 结尾的路由路径的切片。因为只会在以 “/“ 结尾的路由路径中才会出现需要选择最长匹配方案
比如注册的路由有
1 2
| mux.HandleFunc("/a/b/", ab) mux.HandleFunc("/a/", a)
|
那么当一个请求的 URL 为 /a/b/c 的时候,我们希望是由 ab 来处理这个请求。
另外,为了减少在 mux.es 中的查询时间, mux.es 中元素是按照它们的长度由大到小顺序存放
5. ServeHTTP 方法
我们知道在 Go 中要实现一个处理请求的 handler 结构体需要让该结构体现实 Handler 接口的 ServeHTTP 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| type Handler interface { ServeHTTP(ResponseWriter, *Request) }
type myHandler struct {}
func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("This message is from myHandler.")) }
func main() { http.Handle("/", &helloHandler{}) }
|
我们已经通过 Handler 方法拿到了请求(Request)和它对应的处理函数(Handler)
我们的 ServeMux 是一个结构体,它的 ServeHTTP 方法要做的就是将每个请求派遣(dispatch)到它们对应的处理函数上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { if r.RequestURI == "*" { if r.ProtoAtLeast(1, 1) { w.Header().Set("Connection", "close") } w.WriteHeader(StatusBadRequest) return } h, _ := mux.Handler(r) h.ServeHTTP(w, r) }
|
这样,每收到一个请求就会调用对应的处理函数来处理该请求了。
6. 关于 http.HandleFunc 方法
但我们通常会看到,一些简单的示例代码是下面这样写的:
1 2 3 4 5 6 7 8 9
| func helloHandler(w http.ResponseWriter, req *http.Request) { io.WriteString(w, "hello, world! ") }
func main() { http.HandleFunc("/", helloHandler) http.ListenAndServe(":8000", nil) }
|
我们可以看一下 http.HandleFunc 方法做了些什么:
1 2 3
| func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) }
|
可以看到该方法中使用了一个 DefaultServeMux 来注册传入的路由,继续看:
1 2 3
| var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
|
可以看到,http.HandleFunc 也是通过实例化一个全局的 ServeMux 来进行路由注册的。
总结
我们已经了解了 ServeMux 是怎么实现多路处理了,简单概括一下。Handle 和 HandleFunc 方法用来将路由路径与处理函数的映射通过一个 map 记录到当前的 mux 实例里;Handler 方法将接收的请求中的 URL 预处理后拿去和记录的映射匹配,若匹配到,就返回该路由的处理函数和路径;ServeHTTP 方法将请求派遣给匹配到的处理函数处理。
但是 ServeMux 的多路处理实现并不支持请求方法判断,也不能处理路由嵌套和URL变量值提取的功能。